# -*- coding: UTF-8 -*-

# Copyright (C) 2009 Daniele Favara.
# Written by Daniele Favara <daniele@zeroisp.com>.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA


import re

from formencode.api import FancyValidator
from formencode.validators import Invalid, Email, URL, OneOf, MaxLength, \
                                  Regex, NotEmpty, UnicodeString

from tgext.zeroisp.model import ModelError

__all__ = ['ContactAddressValidator', 'EmailValidator', \
           'EnumValidator', 'Invalid', \
           'InvalidContactAddressError', \
           'InvalidEmailError', 'InvalidLocaleError', \
           'InvalidURLError', 'LocaleValidator' , \
           'NotInEnumError', 'OneOf', 'URLValidator', 'SafeURLParameter', \
           'UnicodeString', 'UniqueLanguageSettings', 'ValidationError', \
           'WebsiteAliasDoesntExist', 'XRIValidator']


#{ Validation-related exceptions

class ValidationError(ModelError, Invalid):
    """Base class for validation errors in the model."""


class NotInEnumError(ValidationError):
    """Exception raised when trying to use an invalid value for a L{Enum}
    field."""
    pass


class InvalidLocaleError(ValidationError):
    """Exception for invalid locale strings."""
    pass


class InvalidEmailError(ValidationError):
    """Exception for invalid email addresses."""
    pass


class InvalidURLError(ValidationError):
    """Exception for invalid web addresses."""
    pass


class InvalidContactAddressError(ValidationError):
    """Exception for invalid contact addresses."""
    pass


#{ Static validators

class BaseValidator(FancyValidator):
    """The base class for the validators.

    Every validator must subclass this to override L{validator_exception}.

    """

    validator_exception = ValidationError
    """
    @cvar: The exception raised by your validator.
    @type: L{ValidationError}
    @attention: Replace it with your own exception.
    """

    validators = []
    """
    @cvar: Set of FormEncode-based subvalidators.
    @type: C{list}
    @note: You should define here only your FormEncode-based validators.
    """

    max_length = None
    """
    @cvar: The maximum length of for a value to pass this validator, if
        applicable.
    @type: C{bool} or C{None}
    """

    not_empty = True
    """
    @cvar: May the value be empty?
    @type: C{bool}
    """

    strip = True
    """
    @cvar: Should trailing spaces (if any) be trimmed?
    @type: C{bool}
    """

    def __init__(self, **kwargs):
        """Create a validator specific to this project."""
        super(BaseValidator, self).__init__(**kwargs)
        # Instance validators may have subvalidators different from their
        # class subvalidators.
        self.validators = self.__class__.validators
        # Add the max length validator, if required:
        if self.__class__.max_length:
            length_validator = MaxLength(self.__class__.max_length)
            self.validators.append(length_validator)
        if kwargs.get('not_empty') or self.__class__.not_empty:
            # Only the NotEmpty() validator should take care of empty values
            self.validators.append(NotEmpty())


    def to_python(self, value, state=None):
        """Call the original C{to_python} method, but if the C{Invalid}
        exception is raised, replace it by L{validator_exception}.

        @raise ValidationError: If C{value} is not valid according to the
            parent C{to_python}.

        """
        if not self.not_empty and not value:
            return value
        try:
            return super(BaseValidator, self).to_python(value, state)
        except Invalid, msg:
            raise self.__class__.validator_exception(msg, value, state)

    def validate_python(self, value, state=None):
        """Validate C{value} with the defined subvalidators in LIFO order.

        @raise InvalidEmailError: If the address is invalid according to
            L{to_python}.

        """
        if not self.not_empty and not value:
            return
        validators = self.validators
        validators.reverse()
        try:
            for validator in validators:
                validator.to_python(value, state)
        except Invalid, msg:
            raise self.__class__.validator_exception(msg, value, state)


class EmailValidator(BaseValidator):
    """Validator for emails addresses

    For this project, email addresses must not only be valid, but also they
    must not exceed 255 characters.

    """

    validator_exception = InvalidEmailError

    max_length = 255

    def __init__(self, resolve_domain=False, **kwargs):
        """Create an email validator specific to this project.

        You may use the C{resolve_domain}, which will be passed to FormEncode's
        Email validator.

        @param resolve_domain: Chech that the domain name really exist.
        @type resolve_domain: C{bool}

        """
        super(EmailValidator, self).__init__(**kwargs)
        email_validator = Email(not_empty=kwargs.get('not_empty'),
                                resolve_domain=resolve_domain)
        self.validators.append(email_validator)


class EnumValidator(OneOf):
    """Validator for the L{animador.model.types.Enum} type.

    We're better off subclassing L{OneOf} instead of L{BaseValidator} this
    time.

    """

    def to_python(self, value, state=None):
        """Call the original C{to_python} method, but if the C{Invalid}
        exception is raised, replace it by L{NotInEnumError}.

        @raise NotInEnumError: If the value doesn't belong to the set.

        """
        try:
            return super(EnumValidator, self).to_python(value, state)
        except Invalid, msg:
            raise NotInEnumError(msg, value, state)


class LocaleValidator(BaseValidator):
    """The locale validator"""

    validator_exception = InvalidLocaleError

    validators = [Regex(regex=re.compile('^[a-z]{2}(_[A-Z]{2})?$'))]

    messages = {
        'invalid': '"%(locale)s" is not a valid locale'
        }

    def validate_python(self, value, state=None):
        """Validate the locale.

        @raise InvalidLocaleError: If the locale is not a valid locale,
            according to L{validators}.

        """
        try:
            return super(LocaleValidator, self).validate_python(value, state)
        except InvalidLocaleError, msg:
            raise InvalidLocaleError(self.message("invalid", state,
                                                  locale=value),
                                     value, state)


class URLValidator(FancyValidator):
    """The URL validator.

    @todo: This class must subclass L{BaseValidator}.

    """

    length_validator = MaxLength(255)
    """@cvar: URLs must not exceed this limit."""

    def __init__(self, not_empty=True, check_exists=False, **kwargs):
        """Construct an internal email validator"""
        kwargs['not_empty'] = not_empty
        super(URLValidator, self).__init__(**kwargs)
        self.url_validator = URL(add_http=False, not_empty=not_empty,
                                 check_exists=check_exists)

    def to_python(self, value, state=None):
        """Validate the URL.

        @raise InvalidURLError: If the URL is not valid or exceeds 255
            characters.

        """
        if not self.not_empty and not value:
            return value
        try:
            self.__class__.length_validator.validate_python(value, state)
            return self.url_validator.to_python(value, state)
        except Invalid, msg:
            raise InvalidURLError(msg, value, state)

    def validate_python(self, value, state=None):
        """Validate the URL.

        @raise InvalidURLError: If the address is invalid according to
            L{to_python}.

        """
        self.to_python(value, state)


class ContactAddressValidator(FancyValidator):
    """Validator for L{animador.momdel.types.EContactAddress} objects"""

    not_empty = True

    strip = True

    messages = {
        'no_schema': "Could not find the schema of %(address)s",
        'unsupported_schema': "%(schema)s is not a supported protocol",
        'not_email_jabber_msn': "%(address)s doesn't look like a valid email, "
                                "Jabber or MSN address"
        }

    address_splitter = re.compile(r'^(?P<schema>[a-zA-Z]{2,15}):(?P<id>.+)$')
    """@cvar: The pattern of an address, to extract its schema."""

    def __init__(self, not_empty=True, **kwargs):
        """Create a validator for electronic contact addresses.

        @param not_empty: May the address be empty?
        @type not_empty: C{bool}

        """
        super(ContactAddressValidator, self).__init__(not_empty=not_empty)
        self.email_validator = EmailValidator(not_empty=not_empty, strip=True,
                                              resolve_domain=False)

    @classmethod
    def split_address(cls, address):
        """Split the C{address} into two pieces, the schema and the identifier.

        It returns None if the schema couldn't be found.

        @return: The C{schema} and the C{identifier}.
        @rtype: C{dict} or C{None}

        """
        match = cls.address_splitter.search(address)
        if match is None:
            return None
        return dict(schema=match.group('schema'), identifier=match.group('id'))

    def validate_python(self, address, state=None):
        """Validate an electronic contact address.

        The following schemata are currently supported:
         - C{mailto}
         - C{xmpp}
         - C{msn}

        @rtype: None
        @raise InvalidContactAddressError: If the schema is not supported, or
            if the identifier is invalid for the schema in question, or just
            if L{split_address} could not find the schema.

        """
        parts = self.__class__.split_address(address)
        if parts is None:
            raise InvalidContactAddressError(self.message('no_schema', state,
                                                          address=address),
                                             address, state)
        schema = parts["schema"]
        identifier = parts["identifier"]

        if schema in ('mailto', 'xmpp', 'msn'):
            try:
                self.email_validator.validate_python(identifier)
            except InvalidEmailError:
                raise InvalidContactAddressError(
                    self.message('not_email_jabber_msn', state,
                                 address=address),
                    address, state)
        else:
            raise InvalidContactAddressError(self.message('unsupported_schema',
                                                          state, schema=schema),
                                             schema, state)
        # If reached this point, the address is valid


class XRIValidator(FancyValidator):
    """
    Validator for XRIs.

    It supports both i-names and i-numbers, of the first version of the XRI
    standard.

    @todo: C{Invalid} must be replaced by an Animador exception if this
        validator is not included in FormEncode.

    """
    iname_valid_pattern = re.compile(r"""
    ^
    [\w]+                  # A global alphanumeric i-name
    (\.[\w]+)*             # An i-name with dots
    (\*[\w]+(\.[\w]+)*)*   # A community i-name
    $
    """, re.VERBOSE|re.UNICODE)


    iname_invalid_start = re.compile(r"^[\d\.-]", re.UNICODE)
    """@cvar: These characters must not be at the beggining of the i-name"""

    inumber_pattern = re.compile(r"""
    ^
    (
    [=@]!       # It's a personal or organization i-number
    |
    !!          # It's a network i-number
    )
    [\dA-F]{1,4}(\.[\dA-F]{1,4}){0,3}       # A global i-number
    (![\dA-F]{1,4}(\.[\dA-F]{1,4}){0,3})*   # Zero or more sub i-numbers
    $
    """, re.VERBOSE|re.IGNORECASE)

    messages = {
        'noType': "The type of i-name is not defined; use the equals sign "
                  "for individuals (e.g., '=Bob.Smith'), the at sign for "
                  "organizations (e.g., '@Free.Software.Foundation') or the "
                  "exclamation mark for the i-number of an accredited i-broker "
                  "(e.g., '!!1000!de21.4536.2cb2.8074')",
        'repeatedChar': "Dots and dashes may not be repeated consecutively",
        'badIname': '"%(iname)s" is an invalid i-name',
        'badInameStart': "i-names may not start with numbers nor punctuation "
                         "marks",
        'badInumber': '"%(inumber)s" is an invalid i-number',
        'badType': "The XRI must be a string (not a %(type)s: %(value)r)",
        'badXri': '"%(xri_type)s" is not a valid type of XRI'
        }

    def __init__(self, add_xri=False, xri_type="i-name", **kwargs):
        """Create an XRI validator.

        @param add_xri: Should the schema be added if not present? Officially
            it's optional.
        @type add_xri: C{bool}
        @param xri_type: What type of XRI should be validated? Possible values:
            C{i-name} or C{i-number}.
        @type xri_type: C{str}

        """
        self.add_xri = add_xri
        assert xri_type in ('i-name', 'i-number'), \
                           ('xri_type must be "i-name" or "i-number"')
        self.xri_type = xri_type
        super(XRIValidator, self).__init__(**kwargs)

    def _to_python(self, value, state):
        """Prepend the 'xri://' schema if necessary and then remove trailing
        spaces"""
        if self.add_xri and not value.startswith("xri://"):
            value = "xri://" + value
        return value

    def validate_python(self, value, state=None):
        """Validate an XRI

        @raise Invalid: If at least one of the following conditions in met:
            - C{value} is not a string.
            - The XRI is not a personal, organizational or network one.
            - The relevant validator (i-name or i-number) considers the XRI
                is not valid.

        """
        if not (isinstance(value, str) or isinstance(value, unicode)):
            raise Invalid(self.message("badType", state, type=str(type(value)),
                                       value=value),
                          value, state)

        if value.startswith("xri://"):
            value = value[6:]

        if not value[0] in ('@', '=') and not (self.xri_type == "i-number" \
        and value[0] == '!'):
            raise Invalid(self.message("noType", state), value, state)

        if self.xri_type == "i-name":
            self._validate_iname(value, state)
        else:
            self._validate_inumber(value, state)

    def _validate_iname(self, iname, state):
        """Validate an i-name"""
        # The type is not required here:
        iname = iname[1:]
        if ".." in iname or "--" in iname:
            raise Invalid(self.message("repeatedChar", state), iname, state)
        if self.iname_invalid_start.match(iname):
            raise Invalid(self.message("badInameStart", state), iname, state)
        if not self.iname_valid_pattern.match(iname) or "_" in iname:
            raise Invalid(self.message("badIname", state, iname=iname), iname,
                          state)

    def _validate_inumber(self, inumber, state):
        """Validate an i-number"""
        if not self.__class__.inumber_pattern.match(inumber):
            raise Invalid(self.message("badInumber", state, inumber=inumber,
                                       value=inumber),
                          inumber, state)


class SafeURLParameter(Regex):
    """Validator for URL parameters"""
    regex = re.compile(r'^[\w\-]*$', re.UNICODE)

    strip = True


#{ Runtime validators

class WebsiteAliasDoesntExist(FancyValidator):
    """Check that a given website alias is not in use"""

    messages = {
        'exists': u'The web site alias "%(alias)s" is already in use'
        }

    def validate_python(self, value, state):
        from animador.model.entities import Website
        if Website.query().get(value):
            raise Invalid(self.message("exists", state, alias=value,
                                       value=value),
                          value, state)


class UniqueLanguageSettings(FancyValidator):
    """Check that language-specific settings don't repeat language"""

    messages = {
        'defined': u'Settings are duplicated for language "%(lang)s"'
        }

    def _to_python(self, value, state):
        """Return the settings as a dictionary using the language codes as
        keys"""
        settings = {}
        for setting in value:
            if setting['language'] in settings:
                raise Invalid(self.message("defined", state, value=value,
                                           lang=setting['language']),
                              value, state)
            else:
                settings[setting['language']] = setting
                del settings[setting['language']]['language']
        return settings

#}
